package com.sunny.springboot2.core.config;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.types.Expiration;

import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

/**
 * @author sunny
 * @date 2018/11/29
 */
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisLockHelper {

    private static final String DELIMITER = "|";

    /** 如果有要求可以通过注入的方式分配 */
    private static final ScheduledExecutorService EXECUTOR_SERVICE = new ScheduledThreadPoolExecutor(10, new BasicThreadFactory.Builder().namingPattern("redis-lock-schedule-pool-%d").daemon(true).build());

    private final StringRedisTemplate stringRedisTemplate;

    public RedisLockHelper(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 获取锁（存在死锁风险）
     *
     * @param lockKey lockKey
     * @param value value
     * @param time 超时时间
     * @param unit 过期单位
     * @author sunny
     * @date 2018/11/29
     * @return boolean
     */
    public boolean tryLock(final String lockKey, final String value, final long time, final TimeUnit unit) {
        return stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(lockKey.getBytes(), value.getBytes(), Expiration.from(time, unit), RedisStringCommands.SetOption.SET_IF_ABSENT));
    }

    /**
     * 获取锁
     *
     * @param lockKey lockKey
     * @param uuid UUID
     * @param timeOut 超时时间
     * @param unit 过期单位
     * @author sunny
     * @date 2018/11/29
     * @return boolean
     */
    public boolean lock(String lockKey, final String uuid, long timeOut, final TimeUnit unit) {
        final long milliseconds = Expiration.from(timeOut, unit).getExpirationTimeInMilliseconds();
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);
        if (success) {
            stringRedisTemplate.expire(lockKey, timeOut, TimeUnit.SECONDS);
        } else {
            String oldValue = stringRedisTemplate.opsForValue().getAndSet(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);
            final String[] oldValues = oldValue.split(Pattern.quote(DELIMITER));
            if (Long.parseLong(oldValues[0]) + 1 <= System.currentTimeMillis()) {
                return true;
            }
        }
        return success;
    }

    /**
     * @see <a href="http://redis.io/commands/set">Redis Documentation: SET</a>
     *
     * @param lockKey lockKey
     * @param value value
     * @author sunny
     * @date 2018/11/29
     * @return void
     */
    public void unLock(String lockKey, String value) {
        unLock(lockKey, value, 0, TimeUnit.MICROSECONDS);
    }

    /**
     * 延迟unLock
     *
     * @param lockKey lockKey
     * @param uuid client(唯一键)
     * @param delayTime 延迟时间
     * @param unit 时间单位
     * @author sunny
     * @date 2018/11/29
     * @return void
     */
    public void unLock(final String lockKey, final String uuid, long delayTime, TimeUnit unit) {
        if (StringUtils.isBlank(lockKey)) {
            return;
        }
        if (delayTime <= 0) {
            doUnLock(lockKey, uuid);
        } else {
            EXECUTOR_SERVICE.schedule(() -> doUnLock(lockKey, uuid), delayTime, unit);
        }
    }

    /**
     * 删除key
     *
     * @param lockKey lockKey
     * @param uuid client(唯一键)
     * @author sunny
     * @date 2018/11/29
     * @return void
     */
    private void doUnLock(String lockKey, String uuid) {
        String val = stringRedisTemplate.opsForValue().get(lockKey);
        final String[] values = val.split(Pattern.quote(DELIMITER));
        if (values.length <= 0) {
            return;
        }
        if (uuid.equals(values[1])) {
            stringRedisTemplate.delete(lockKey);
        }
    }
}
